{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 前言-学习本项目你可以获得什么\n",
    "- 理论学习：了解MCP Client的基础知识\n",
    "- 上手实操：了解MCP Client的使用方式\n",
    "- 上手实操：通过MCP Client连接MCP Server，结合已有Agent，打造一个端云结合的进阶Agent。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 1. 项目背景\n",
    "### 1.1 什么是MCP\n",
    "[Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction)是Anthropic推出的一个开放协议，提供了一种将 AI 模型连接到不同数据源和工具的标准化方式，可实现应用与外部数据源和工具之间的无缝集成。\n",
    "\n",
    "MCP基础架构由以下几部分组成：\n",
    "- MCP Hosts: 希望通过 MCP 访问数据的程序，例如 Claude Desktop、IDE 或 AI 工具\n",
    "- MCP Clients: 与服务器保持 1:1 连接的协议客户端\n",
    "- MCP Servers: 通过MCP协议公开特定功能的程序\n",
    "- Local Data Sources: MCP协议可以安全访问的本机文件、数据库、服务等\n",
    "- Remote Services: MCP Server可以访问的远程服务\n",
    "\n",
    "其中MCP Server包含以下几类：\n",
    "- Resources: 上下文和数据，供用户或人工智能模型使用\n",
    "- Prompts: 为用户提供模板化的消息和工作流程\n",
    "- Tools: AI模型调用的工具，使模型能够与外部系统交互，例如查询数据库、调用 API 或执行计算\n",
    "\n",
    "MCP Server Tools有以下特征：\n",
    "- 每个工具都由名称唯一标识，并包含描述其架构的元数据\n",
    "- 被设计为模型控制的，这意味着语言模型可以根据其上下文理解和用户的提示自动发现和调用工具\n",
    "\n",
    "### 1.2 MCP Client\n",
    " Host 内部专门用于与 MCP Server 建立和维持一对一连接的模块。它负责按照 MCP 协议的规范发送请求、接收响应和处理数据。简单来说，MCP Client 是 Host 内部处理 RPC 通信的“代理”，专注于与一个 MCP Server 进行标准化的数据、工具或 prompt 的交换。\n",
    " \n",
    " MCP Client 更多是一个底层技术术语，是关于 MCP Server 连接到 MCP Host 的底层细节，不用过于区分 MCP Host 和 MCP Client。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 2. 基础操作-通过MCP Client连接Server\n",
    "## 2.1 stdio方式连接"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "【第一步】安装AppBuilder的环境依赖，在python>=3.12环境中，执行以下命令。Python环境准备可以参考教程：https://cloud.baidu.com/article/3421098"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "vscode": {
     "languageId": "shellscript"
    }
   },
   "outputs": [],
   "source": [
    "!python3 -m pip install appbuilder-sdk -i https://mirrors.aliyun.com/pypi/simple/\n",
    "!python3 -m pip install mcp -i https://mirrors.aliyun.com/pypi/simple/"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "【第二步】以[官方server](https://github.com/modelcontextprotocol/quickstart-resources/blob/main/weather-server-python/weather.py)为例。我们将server代码保存为`weather.py`。这个server包含`get_alerts', 'get_forecast'，两个tool。\n",
    "\n",
    "再编写client脚本："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import asyncio\n",
    "from appbuilder.mcp_server.client import MCPClient\n",
    "\n",
    "\n",
    "async def main():\n",
    "    mcp_client = MCPClient()\n",
    "    await mcp_client.connect_to_server(\"./weather.py\")\n",
    "    print(mcp_client.tools)\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    loop = asyncio.get_event_loop()\n",
    "    loop.run_until_complete(main())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "运行后，打印日志参考：\n",
    "\n",
    "Connected to server with tools:['get_alerts', 'get_forecast']\n",
    "\n",
    "INFO:appbuilder:\n",
    "\n",
    "Connected to server with tools:['get_alerts', 'get_forecast']\n",
    "[Tool(name='get_alerts', description='Get weather alerts for a US state.\\n\\n    Args:\\n        state: Two-letter US state code (e.g. CA, NY)\\n    ', inputSchema={'properties': {'state': {'title': 'State', 'type': 'string'}}, 'required': ['state'], 'title': 'get_alertsArguments', 'type': 'object'}), Tool(name='get_forecast', description='Get weather forecast for a location.\\n\\n    Args:\\n        latitude: Latitude of the location\\n        longitude: Longitude of the location\\n    ', inputSchema={'properties': {'latitude': {'title': 'Latitude', 'type': 'number'}, 'longitude': {'title': 'Longitude', 'type': 'number'}}, 'required': ['latitude', 'longitude'], 'title': 'get_forecastArguments', 'type': 'object'})]\n",
    "\n",
    "## 2.2 sse方式连接\n",
    "以AppBuilder百度搜索MCP Server为例，server地址`http://appbuilder.baidu.com/v2/ai_search/mcp/sse?api_key=Bearer+bce-v3/ALTAKxxx`, api_key为AppBuilder Token，格式参考URL中格式。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import asyncio\n",
    "from appbuilder.mcp_server.client import MCPClient\n",
    "\n",
    "\n",
    "async def main():\n",
    "    client = MCPClient()\n",
    "    await client.connect_to_server(service_url=service_url)\n",
    "    print(client.tools)\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    service_url = \"http://appbuilder.baidu.com/v2/ai_search/mcp/sse?api_key=Bearer+bce-v3/ALTAKxxx\"\n",
    "    loop = asyncio.get_event_loop()\n",
    "    loop.run_until_complete(main())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "运行后，打印日志参考：\n",
    "\n",
    "Connected to server with tools:['AIsearch']\n",
    "INFO:appbuilder:\n",
    "Connected to server with tools:['AIsearch']\n",
    "[Tool(name='AIsearch', description='\\n    执行搜索。\\n\\n    Args:\\n        query (str): 搜索请求。\\n        stream (bool, optional): 是否以流的形式接收响应数据。默认为False。\\n        instruction (Instruction, optional): 指令信息对象。默认为None。\\n        model (str, optional): 模型名称。默认为None，表示使用当前实例的模型。\\n        temperature (float, optional): 温度参数，控制生成文本的随机性。默认为1e-10。\\n        top_p (float, optional): 累积概率阈值，用于控制生成文本的多样性。默认为1e-10。\\n        search_top_k (int, optional): 搜索候选结果的数量。默认为4。\\n        hide_corner_markers (bool, optional): 是否隐藏响应中的边界标记。默认为True。\\n\\n    Returns:\\n        Message: 处理后的信息对象。\\n\\n    Raises:\\n        AppBuilderServerException: 如果输入信息或指令过长，将抛出此异常。\\n    ', inputSchema={'properties': {'query': {'title': 'query', 'type': 'string'}, 'stream': {'default': False, 'title': 'stream', 'type': 'string'}, 'instruction': {'default': None, 'title': 'instruction', 'type': 'string'}, 'model': {'default': None, 'title': 'model', 'type': 'string'}, 'temperature': {'default': 1e-10, 'title': 'temperature', 'type': 'string'}, 'top_p': {'default': 1e-10, 'title': 'top_p', 'type': 'string'}, 'search_top_k': {'default': 4, 'title': 'search_top_k', 'type': 'string'}, 'hide_corner_markers': {'default': True, 'title': 'hide_corner_markers', 'type': 'string'}, 'api_key': {'default': None, 'title': 'api_key', 'type': 'string'}}, 'required': ['query'], 'title': 'AIsearchArguments', 'type': 'object'})]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 3. 进阶操作-Agent 应用轻松调用海量开源 MCP Server，打造端云结合的进阶 Agent\n",
    "搭建一个“端云组件结合使用”的新闻检索 Agent ：功能是，用户检索感兴趣的新闻，Agent 根据 query 进行检索，最终自动调用 Playwright 组件在本地打开浏览器，在mac上阅览。对比常规的Agent，区别是：\n",
    "\n",
    "* Before：Agent在聊天界面中展示新闻链接，提供总结信息\n",
    "* After：Agent在此基础上，还可以在本地的浏览器中自动打开指定的网页，进行本地操作\n",
    "为什么使用这个示例：\n",
    "\n",
    "该应用可以扩展为：用户询问需求后，Agent不仅思考并回答，还能操作用户的终端，帮助用户下订单、操作文件等等\n",
    "\n",
    "### 3.1 【第一步】：创建云端的Agent，作为基础的应用，步骤如下：\n",
    "* 打开AppBuilder官网：https://qianfan.cloud.baidu.com/appbuilder/ ，并登录\n",
    "![AppBuilder首页](./image/appbuilder_home.png)\n",
    "\n",
    "* 在左侧的边栏中，选择创一个【自主规划Agent】\n",
    "![创建自主规划Agent](./image/agent_create.png)\n",
    "\n",
    "* 使用【AI自动配置功能】生成一个自动配置好角色指令的Agent，Prompt可以是：【热点新闻阅读助手】\n",
    "![创建热点新闻阅读助手](./image/agent_config.png)\n",
    "\n",
    "* 可以调整该应用的组件，我们选择【百度AI搜索】作为提供网页url的来源，最终Agent界面长下面这个样子\n",
    "![热点新闻阅读助手](./image/agent_show.png)\n",
    "\n",
    "* 继续调整该组件，考虑到我们可能会多次思考，多次调用组件，所以，我们调整最大思考轮数\n",
    "![最大思考轮数](./image/agent_max_rounds.png)\n",
    "\n",
    "* 最后，点击右上角【发布】->【发布应用】，这样我们就可以在AB-SDK中调用该应用啦\n",
    "![发布应用](./image/agent_pub.png)\n",
    "\n",
    "\n",
    "### 3.2 【第二步】：下载一个可以控制本地浏览器的MCP-Server组件Playwright，步骤如下\n",
    "我们使用[Playwright](https://github.com/blackwhite084/playwright-plus-python-mcp/blob/master/src/playwright_server/server.py )作为MCP Server，直接保存该代码到本地可以执行的位置。\n",
    "![下载MCP Server](./image/playwright_download.png)\n",
    "\n",
    "\n",
    "### 3.3 【第三步】：使用AB-SDK访问我们的Agent，并且让它联动本地的MCP组件\n",
    "* 通过代码态访问我们刚创建的Agent，有以下几个依赖的配置，首先是【APP ID】，获取方式如下：\n",
    "![获取app_id](./image/app_get.png)\n",
    "* 然后是访问应用所需要的Token，获取方式如下：\n",
    "![获取Token](./image/token_get.png)\n",
    "* 创建Token时记得需要勾选我们刚才创建的【新闻小助手】应用\n",
    "![Token功能范围](./image/token_scope.png)\n",
    "* python >= 3.12 的环境中安装 【appbuilder-sdk】及相关MCP组件的依赖\n",
    "\n",
    "安装playwright支持的浏览器，该步骤由于网络原因，时间较长，约5-10min，可通过代理提速\n",
    "\n",
    "```bash\n",
    "python -m pip install appbuilder-sdk -i https://pypi.tuna.tsinghua.edu.cn/simple \n",
    "python -m pip install mcp -i https://pypi.tuna.tsinghua.edu.cn/simple\n",
    "python -m pip install playwright -i https://pypi.tuna.tsinghua.edu.cn/simple \n",
    "playwright install \n",
    "```\n",
    "\n",
    "* 有了【APP_ID】和【TOKEN】，并且在本地安装好上述依赖后，我们可以快速访问【新闻小助手应用】，搜索感兴趣的新闻，并在本地浏览器打开对应网页"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import time\n",
    "import asyncio\n",
    "\n",
    "import appbuilder\n",
    "from appbuilder.core.console.appbuilder_client.async_event_handler import (\n",
    "    AsyncToolCallEventHandler,\n",
    ")\n",
    "\n",
    "from appbuilder.mcp_server.client import MCPClient\n",
    "\n",
    "os.environ[\"APPBUILDER_TOKEN\"] = \"YOUR_APPBUILDER_TOKEN\"\n",
    "os.environ[\"APP_ID\"] = \"YOUR_APP_ID\"\n",
    "\n",
    "\n",
    "async def main():\n",
    "    app_id = os.environ.get(\"APP_ID\")\n",
    "    assert app_id is not None, \"APP_ID is not set\"\n",
    "\n",
    "    appbuilder_client = appbuilder.AsyncAppBuilderClient(app_id)\n",
    "    mcp_client = MCPClient()\n",
    "    # server.py 是上述步骤中下载的mcp组件文件\n",
    "    await mcp_client.connect_to_server(\"./server.py\")\n",
    "\n",
    "    tools = mcp_client.tools\n",
    "    event_handler = AsyncToolCallEventHandler(mcp_client, functions=[])\n",
    "    conversation_id = await appbuilder_client.create_conversation()\n",
    "    with await appbuilder_client.run_with_handler(\n",
    "        conversation_id=conversation_id,\n",
    "        query=\"先搜索关于文心一言4.5模型的新闻，取出其中一个url，再用playwright_navigate打开这个url\",\n",
    "        tools=tools,\n",
    "        event_handler=event_handler,\n",
    "    ) as run:\n",
    "        await run.until_done()\n",
    "\n",
    "    print(\"浏览网页，我们在此稍作停留，您可通过其他方式常驻该进程以保持网页\")\n",
    "    time.sleep(5)\n",
    "    await appbuilder_client.http_client.session.close()\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    appbuilder.logger.setLoglevel(\"DEBUG\")\n",
    "    loop = asyncio.get_event_loop()\n",
    "    loop.run_until_complete(main())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "* 在终端执行上述代码\n",
    "\n",
    "**效果展示**\n",
    "\n",
    "![大模型结果](./image/llm_answer.png)\n",
    "\n",
    "* 界面效果\n",
    "  * 自动启动了一个chrome浏览器，并打开了我们搜索到的网页\n",
    "![playwright结果](./image/playwright_result.png)"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
